---
id: history-css-in-js
title: CSS-in-JS 阶段
description: 动态样式、运行时开销与 SSR 考量
slug: /tailwindcss/history/css-in-js
sidebar_position: 5
---

## 要点

- 组件边界天然隔离，props 可驱动样式；动态主题/状态友好。
- 成本：运行时/SSR 注水体积、构建链复杂；类名可读性差。
- 适合需要高度动态样式、主题切换、设计系统与组件强绑定的团队。
- 代表性包：运行时流派（`styled-components`、`Emotion`、`JSS`）、编译期/零运行时流派（`vanilla-extract`、`Linaria`、`Astro/Uno + scoped` 混合方案）。

## 优势 / 劣势 / 何时使用

| 项 | 内容 |
| --- | --- |
| 优势 | 组件粒度隔离；props 驱动；可在 JS 里复用逻辑/常量 |
| 劣势 | 运行时开销；SSR 注水；类名调试性差；可能影响 HMR 速度 |
| 适用 | 需要复杂动态样式、主题即逻辑的设计系统；部分 SSR/CSR 混合场景 |
| 不适用 | 对首屏体积极敏感、运行时预算极小的多端/小程序场景 |

## 代表性包与用法

- **styled-components（运行时）**：模板字符串 + props；支持 `ThemeProvider` 统一主题。

```tsx title="styled-components 主题"
import styled, { ThemeProvider } from 'styled-components'

const theme = { primary: '#111827', radius: '12px' }
const Button = styled.button`
  padding: 10px 16px;
  border-radius: ${({ theme }) => theme.radius};
  background: ${({ theme }) => theme.primary};
`

export function Demo() {
  return (
    <ThemeProvider theme={theme}>
      <Button>Dark Button</Button>
    </ThemeProvider>
  )
}
```

- **Emotion（运行时 + 编译模式）**：`css` prop 与 `@emotion/babel-plugin` 编译模式减少运行时。

```tsx title="Emotion css prop"
import { css } from '@emotion/react'

const card = css({
  border: '1px solid #e5e7eb',
  borderRadius: 16,
  padding: 16,
})

export const Card = () => <section css={card}>Emotion css prop</section>
```

- **vanilla-extract（零运行时）**：TypeScript API 生成 CSS，运行时仅使用 className。

```ts title="vanilla-extract button.css.ts"
import { style, createVar } from '@vanilla-extract/css'

export const color = createVar()
export const button = style({
  vars: { [color]: '#111827' },
  background: color,
  borderRadius: '8px',
  color: '#fff',
})
```

## <span className="mr-2 inline-block align-middle text-primary dark:text-primary-200 icon-[mdi--layers-triple-outline]"></span> 运行时 vs 编译期（示意）

```mermaid
%%{ init: { "flowchart": { "curve": "basis" } } }%%
flowchart LR
  A["源码：styled-components / Emotion"] --> B{"运行时注入？"}
  B -->|是| C["客户端生成 style 标签\n首屏注水 + HMR 成本"]
  B -->|否| D["编译期生成 CSS\nvanilla-extract / Linaria 产出静态 CSS"]
  D --> E["仅 className 传递\n运行时开销小"]
```

<div className="mt-3 grid gap-2 text-sm text-muted-foreground">
  <div className="flex items-start gap-2">
    <span className="icon-[mdi--download] text-lg text-primary dark:text-primary-200" aria-hidden="true" />
    <span>运行时注入：在客户端生成 style，首屏注水 + HMR 有额外成本。</span>
  </div>
  <div className="flex items-start gap-2">
    <span className="icon-[mdi--file-code-outline] text-lg text-primary dark:text-primary-200" aria-hidden="true" />
    <span>编译期生成：静态 CSS 提前产出，JS 只保留 className 映射。</span>
  </div>
</div>

### 写法与产物对照

- 运行时（styled-components/Emotion）：
  - 写法：模板字符串或对象样式，允许使用 props / theme 计算样式；开发态插入 `<style>`，生产态可能提取 critical CSS。
  - 产物：JS bundle + 内联 style 标签，类名在运行时生成（`sc-abc123`），首屏注水与 HMR 需要样式注入开销。
- 编译期（Linaria/vanilla-extract）：
  - 写法：受限的模板字符串或 TS API（`css`/`style`），编译器提前求值并生成 `.css`，JS 中只保留 `className` 映射。
  - 产物：静态 CSS 文件（或内联 chunk）+ 极薄的 className 映射，运行时不再注入 style，SSR 直接 link CSS。

### Linaria 示例（编译期）

源码（编译前）：

```tsx title="card.tsx"
import { css } from '@linaria/core'

const card = css`
  border: 1px solid #e5e7eb;
  border-radius: 16px;
  padding: 16px;
  background: white;
  transition: box-shadow 150ms ease;

  &:hover {
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.06);
  }
`

export function Card({ children }: { children: React.ReactNode }) {
  return <section className={card}>{children}</section>
}
```

编译产物（示意）：

```css title="card.linaria.css"
.card_h3dj1z {
  border: 1px solid #e5e7eb;
  border-radius: 16px;
  padding: 16px;
  background: white;
  transition: box-shadow 150ms ease;
}
.card_h3dj1z:hover {
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.06);
}
```

```js title="card.tsx (compiled excerpt)"
import './card.linaria.css'
const card = 'card_h3dj1z'
export function Card({ children }) {
  return <section className={card}>{children}</section>
}
```

特点：CSS 被提前生成，JS 仅保留类名字符串；运行时不再注入 `<style>`，与传统静态 CSS 链路类似。

## 示例（styled-components）

```tsx
import styled from 'styled-components'

const Card = styled.section`
  border: 1px solid #e5e7eb;
  border-radius: 16px;
  padding: 16px;
  box-shadow: ${({ elevated }) => (elevated ? '0 10px 30px rgba(0,0,0,0.06)' : 'none')};
`

const Button = styled.button`
  padding: 10px 16px;
  border-radius: 8px;
  border: 1px solid #111827;
  background: #111827;
  color: #fff;
  &:hover { background: #0f172a; }
`

export const Demo = () => (
  <Card elevated>
    <p className="eyebrow">CSS-in-JS</p>
    <h2>动态样式，组件边界</h2>
    <Button>查看详情</Button>
  </Card>
)
```

## 常见坑与对策

- 运行时体积：优先选择编译模式或零运行时方案（如 vanilla-extract），或开启 Babel SWC 优化。
- SSR 注水：衡量首屏样式注入体积；必要时提取静态样式或开启样式缓存。
- 类名调试：在开发环境打开 displayName/label；在生产环境启用最小化。
- HMR 性能：减少过深的动态表达式；拆分组件，降低热更范围。
